#include <fstream>
#include <sstream>
#include <boost/regex.hpp>
#include <pthread.h>
#include <fcntl.h>
#include <sys/inotify.h>

#include "xpath_static.h"
#include "tinyxml.h"
#include "listeningsocket.h"
#include "connectingsocket.h"
#include "exception.hpp"
#include "exception_error.hpp"
#include "exception_closed.hpp"
#include "exception_xml.hpp"
#include "lib.h"
#include "logger.h"
#include "student.h"
#include "database.h"
#include "login_manager.h"
#include "user.h"
#include "message_connection_socket.h"
#include "plain_text_message.h"

#define BUFF_SIZE ( ( sizeof ( struct inotify_event ) + FILENAME_MAX ) * 1024 )

typedef std::vector< std::pair< TiXmlBase*, bool > > XMLNodeSet;

int nClientFD;

//TODO the sMessage argument should not be copied when we call this function
//TODO There's no need to complicate the function name. SendMessage() is clear enough
void SendMessageFunc( utils::ConnectionSocket* pConnectionSocket, std::string sMessage )
{
	connection::PlainTextMessage* pMsg = new connection::PlainTextMessage( sMessage );
	connection::PlainTextSerializer* pSerializer = new connection::PlainTextSerializer();
		MessageConnectionSocket* pMessageConnectionSocket = dynamic_cast<MessageConnectionSocket*>(pConnectionSocket);

	pMessageConnectionSocket->SendMessage( pMsg, pSerializer );
	delete pMsg;
}

//TODO although not mandatory, it'd be more clear if this function returns the received message, rather than setting it as an OUT paramter
//TODO There's no need to complicate the function name. ReceiveMessage() is clear enough
void ReceiveMessageFunc( connection::PlainTextSerializer* pSerializer, utils::ConnectionSocket* pConnectionSocket, std::string& sMessage )
{
	MessageConnectionSocket* pMessageConnectionSocket = dynamic_cast<MessageConnectionSocket*>(pConnectionSocket);
	connection::PlainTextMessage* pMsg = pMessageConnectionSocket->ReceiveMessage( pSerializer );
	sMessage = pMsg->GetMessage();
	delete pMsg;
}


void ParseCommand( std::string& sRequestCommand, std::string& sCommand, std::string& sParameter )
throw( utils::Exception )
{
	const boost::regex rxExpression( "^\\s*([^\\s]*)\\s*([^\r\n]*)\\s*" );
	boost::smatch rxMatch;
	if ( regex_match( sRequestCommand, rxMatch, rxExpression ) )
	{
		sCommand = rxMatch[1];
		sParameter = rxMatch[2];
	}

	else
	{
		throw utils::Exception( "Can't match sRequestCommand" );
	}

}

std::string GetPrintableStudentInformation( Student* pStudent )
{
        std::stringstream ssResult;

        ssResult << "Name: " << pStudent->GetName() << std::endl;
        ssResult << "Surname: " << pStudent->GetSurname() << std::endl;
        ssResult << "Familyname: " << pStudent->GetFamilyName()<< std::endl;
        ssResult << "EGN: " << pStudent->GetEGN() << std::endl;
        ssResult << "FN: " << pStudent->GetFN() << std::endl;
        ssResult << "Year: " << pStudent->GetYear() << std::endl;
        ssResult << "Average Mark: " << pStudent->GetAverageMark() << std::endl;
        ssResult << "Disciplines: " << pStudent->GetDiscipline() << std::endl;

        return ssResult.str();
}

void* ReloadFile( void* threadid ) throw ( utils::Exception )
{
	DataBase* pDB = static_cast<DataBase*>(threadid);

	std::string sFilePath = pDB->GetFilePath();

	int fdMyFile = access( sFilePath.c_str(), O_RDWR );

	if( fdMyFile < 0 )
	{
		throw utils::Exception( "Error opening file" );
	}

	char chTarget[ FILENAME_MAX ];
	int nFileDescriptor;
	int nWatchDescriptor;

	strcpy( chTarget, sFilePath.c_str() );

	nFileDescriptor = inotify_init();
	nWatchDescriptor = inotify_add_watch( nFileDescriptor, chTarget, IN_MODIFY | IN_ATTRIB );

	while( true )
	{
		ssize_t len = 0;
		ssize_t counter = 0;
		char chBuff[ BUFF_SIZE ] = { 0 };

		len = read( nFileDescriptor, chBuff, BUFF_SIZE );

		while( counter < len )
		{
			struct inotify_event* pevent = (struct inotify_event*) &chBuff[ counter ];

			if( pevent->mask & IN_ATTRIB )
			{
				pDB->Reload();
			}
			if ( pevent -> mask & IN_MODIFY )
			{
				pDB->Reload();
			}

			counter += sizeof( struct inotify_event ) + pevent->len;
		}
	}

	inotify_rm_watch( nFileDescriptor, nWatchDescriptor );
	close( nFileDescriptor );
	pthread_exit( NULL );
}

struct ConnectionHandlerThreadData
{
	utils::ConnectionSocket* pConnectionSocket;
	DataBase* pDataBase;
	LoginManager* pLoginManager;
};


void* ConnectionHandler( void* threadid ) throw( utils::Exception )
{
	ConnectionHandlerThreadData* pThreadData = static_cast<ConnectionHandlerThreadData*>(threadid);
	utils::ConnectionSocket* pConnectionSocket = pThreadData->pConnectionSocket;
	DataBase* pStudentsDatabase = pThreadData->pDataBase;
	LoginManager* pLoginManager = pThreadData->pLoginManager;
	delete pThreadData;

	std::string sAvailableUsers;
	sAvailableUsers = "Registered users: " + pLoginManager->GetAvailableUsers() + "\nPlease enter user: ";

	connection::PlainTextSerializer* pSerializer = new connection::PlainTextSerializer();

	SendMessageFunc( pConnectionSocket, sAvailableUsers );

	std::string sOutput;
	std::string sUserInput;
	std::string sPasswordInput;

	try
	{
		ReceiveMessageFunc( pSerializer, pConnectionSocket, sUserInput );
	}
	catch( utils::ExceptionError& e )
	{
        //TODO since nothing else, but logging is done within the catch clauses, they should be moved inside the ReceiveMessageFunc
		ERR << "Error. Reason: " << e.GetMessage() << std::endl;
	}
	catch( utils::ExceptionClosed& e )
	{
		ERR << "Error. Reason: " << e.GetMessage() << std::endl;
	}


	DBG << "User received: " << sUserInput  << std::endl;

	if( sUserInput[ sUserInput.length()-1 ] == '\n' )
	{
		sUserInput.erase( sUserInput.length() - 1 );
	}

	User* pUser = pLoginManager->Search( sUserInput );

	SendMessageFunc( pConnectionSocket, "Please enter password: " );

	try
	{
		ReceiveMessageFunc( pSerializer, pConnectionSocket, sPasswordInput );
	}
	catch( utils::ExceptionError& e )
	{
        //TODO --||--
		ERR << "Error. Reason: " << e.GetMessage() << std::endl;
	}
	catch( utils::ExceptionClosed& e )
	{
		ERR << "Error. Reason: " << e.GetMessage() << std::endl;
	}


	DBG << "Password received: " << sPasswordInput << std::endl;

	if( sPasswordInput[ sPasswordInput.length()-1 ] == '\n' )
	{
		sPasswordInput.erase( sPasswordInput.length() - 1 );
	}

    //TODO bright new idea :) the GetUser() method (previosly named Search()) could receive the sUsername and sPassword and return NULL if the user is not valid or if it's valid, but the password is invalid. This way we hide all authentication login inside the login manager (hence it does all the job it's supposed to do)
	if( NULL == pUser || !( pUser->CheckPassword( sPasswordInput ) ) )
	{
		sOutput = "Error! Invalid user or password!\n";
		SendMessageFunc( pConnectionSocket, sOutput );

        //TODO hm, it looks like a function could be used here - ExitConnectionThread( pSerializer, pConnectionSocket, .... );
		DBG << "Connection thread exiting" << std::endl;
		delete pSerializer;
		delete pConnectionSocket;

		pthread_exit( NULL );
	}

	SendMessageFunc( pConnectionSocket, "Welcome " + sUserInput + "!\nType 'help' for more information! \n" );

	while( true )
	{
		std::string sRequestCommand;

		try
		{
			ReceiveMessageFunc( pSerializer, pConnectionSocket, sRequestCommand );
		}
		catch( utils::ExceptionError& e )
		{
            //TODO - this has been mentioned before
			ERR << "Error. Reason: " << e.GetMessage() << std::endl;
		}
		catch( utils::ExceptionClosed& e )
		{
			ERR << "Error. Reason: " << e.GetMessage() << std::endl;
			break;
		}


		DBG << "Command received: '" << sRequestCommand << "'" << std::endl;

		std::string sOutput;
		std::string sCommand;
		std::string sParameter;

		try
		{
			ParseCommand( sRequestCommand, sCommand, sParameter );
			DBG << "Parsed request: command='" << sCommand
			<<"', parameter='" << sParameter << "'" << std::endl;

            //TODO how about we call pUser->CheckAvailableCommands( sCommand ) and if the command is not allowed - simply notify the user and ask them to enter another command
            //There is no need to check whether the command is allowed in each if. The command does not change anyway. 
			if( sCommand =="help" && pUser->CheckAvailableCommands( sCommand ) )
			{
				sOutput = "\n=Help Menu=\n\nAll commands: \nSee all available FNs: type 'all' \nGet student information: type 'showfn FN' \nClose current session: type 'close' \nShutdown the server: type 'shutdown' \n\nAvailable commands for " + sUserInput + ": " + pUser->GetAvailableCommands() + "\n";
			}
			else if( sCommand =="all" && pUser->CheckAvailableCommands( sCommand ) )
			{
				sOutput = "Available FNs: " + pStudentsDatabase->GetAvailableFNsPrintable();
			}
			else if( sCommand =="showfn" && pUser->CheckAvailableCommands( sCommand )  )
			{
                //TODO if the student object is available, we search for it twice :) should be reworked a bit.
				if( pStudentsDatabase->Search( sParameter ) == NULL )
				{
					sOutput = "Invalid Fn! See all available FNs: type 'all'. \n";
				}
				else
				{
					sOutput = GetPrintableStudentInformation( pStudentsDatabase->Search( sParameter ) );
				}
			}
			else if( sCommand =="close" && pUser->CheckAvailableCommands( sCommand ) )
			{
				break;
			}
			else if( sCommand =="shutdown" && pUser->CheckAvailableCommands( sCommand ) )
			{
				exit(0);
			}
			else if( sCommand.empty() )
			{
				sOutput = "";
			}
			else
			{
				sOutput = "Error! Invalid Command! Please type 'help' for more information.";
			}

			MSG << "Responding with: '" << sOutput << "'" << std::endl;
			sOutput.push_back('\n');

			SendMessageFunc( pConnectionSocket, sOutput );

		}
		catch( utils::Exception& e )
		{
			ERR << "Error. Reason: " << e.GetMessage() << std::endl;
            //TODO in case of error no one will send this output to the user... the call to SendMessageFunc( pConnectionSocket, sOutput ); should be after the catch, instead of inside the try :)
			sOutput = e.GetMessage();
		}
	}

	DBG << "Connection thread exiting" << std::endl;
	delete pSerializer;
	delete pConnectionSocket;

	pthread_exit( NULL );

	DBG << "Reached end of thread 1... " << std::endl;
}

int main( int argc, char* argv[] )
{
	DBG << "Program started at " << utils::GetCurrentTimeString() << std::endl;

	DBG << "Argument(s) : " << argc << std::endl;
	for( int i = 0; i < argc; i++ )
	{
		DBG << "argv[" << i << "] = " << argv[i] << std::endl;
	}

	if( 2 != argc )
	{
		ERR << "Not enough or invalid arguments, please try again.\n";
	}

	else
	{
		std::string sPath;
		TiXmlDocument xmlDoc;
		xmlDoc.LoadFile( argv[1] );

		if ( xmlDoc.Error() )
		{
			std::cerr << "Error loading XML at [ "
			<< xmlDoc.ErrorRow() << ", "
			<< xmlDoc.ErrorCol() << " ]: "
			<< xmlDoc.ErrorDesc() << std::endl;
			exit(1);
		}

		TiXmlElement* pRootElement = xmlDoc.RootElement();

		XMLNodeSet vecBase;
		utils::GetXMLNodeSetAtPath(
		pRootElement,
		"/configuration/db_file/db_file/@path",
		vecBase );


		for( XMLNodeSet::const_iterator cit = vecBase.begin();
		vecBase.end() != cit;
		++cit )
		{
			TiXmlBase* pBase = cit->first;
			bool bIsAttribute = cit->second;
			if( bIsAttribute )
			{
				TiXmlAttribute* pAttr = static_cast<TiXmlAttribute*>(pBase);
				sPath = pAttr->Value();
			}
		}


		DataBase* pStudentsDatabase = new DataBase( sPath );
		LoginManager* pLoginManager = new LoginManager( argv[1] );


		pthread_t thread;
		int rc = pthread_create( &thread, NULL, ReloadFile , pStudentsDatabase );
		if( rc )
		{
			ERR << "pthread_create() failed. Result code: " << rc << std::endl;
			exit( rc );
		}

		MSG << "The thread started" << std::endl;

		MSG << "Server running..." << std::endl;

		utils::ListeningSocket* pListeningSocket = new utils::ListeningSocket( 3456 );

		try
		{
			try
			{
				while( true )
				{
					DBG << "Listening for new connections" << std::endl;
					nClientFD = pListeningSocket->Accept();
					DBG << "New connection accepted" << std::endl;

					ConnectionHandlerThreadData* pConnHandlerThreadData = new ConnectionHandlerThreadData();
					pConnHandlerThreadData->pConnectionSocket = new MessageConnectionSocket( nClientFD );
					pConnHandlerThreadData->pDataBase = pStudentsDatabase;
					pConnHandlerThreadData->pLoginManager = pLoginManager;

					pthread_t ConnectionHandlerThread;
					int nThreadResult = pthread_create( &ConnectionHandlerThread, NULL, ConnectionHandler, pConnHandlerThreadData );
					if( nThreadResult )
					{
						ERR << "pthread_create() failed. Result code: " << nThreadResult << std::endl;
						exit( nThreadResult );
					}
				}
			}
			catch( utils::Exception& e )
			{
				ERR << "Error serving client connection: " << e.GetMessage() << std::endl;
			}
		}
		catch( utils::Exception& e )
		{
			MSG << "Runtime exception:" << e.GetMessage() << std::endl
				<< "Exiting" << std::endl;
		}

		delete pListeningSocket;

		pthread_join( thread, NULL );
		MSG << "Joined thread" << std::endl;
	}

	return 0;

}

